﻿// (c) Copyright 2013 Mike Riedel.
// This source is subject to the Microsoft Public License (Ms-PL).
// All other rights reserved.

using System;

namespace FreshMeat.MiniHsm
{
   /// <summary>
   /// A lightweight implementation of a Hierarchical State Machine (HSM).
   /// </summary>
   /// <remarks>
   /// <para>
   /// This project was inspired by Miro Samek's minimalist approach to an HSM 
   /// (see http://www.accu-usa.org/Slides/samek0311.pdf) but it is not a port.
   /// This HSM adheres to the UML rule that transition actions must occur after
   /// exit actions and before entry actions, allows derived machines to choose
   /// any event type and to dictate the built-in event values all without casting,
   /// supports validation to ensure derived states are implemented correctly and 
   /// is otherwise easier to use and customize.
   /// </para>
   /// <para>
   /// See http://minihsm.codeplex.com for details, including documentation and usage.
   /// </para>
   /// </remarks>
   public abstract class MiniMachine<TEvent>
   {
      readonly TEvent getParentEvent;
      readonly TEvent setDefaultEvent;
      readonly TEvent enterEvent;
      readonly TEvent exitEvent;
      readonly State[] targetPath;
      State currentState;
      ExpectedCall expectedCall;

      /// <summary>
      /// Indicates whether <see cref="OnStateChanged"/> should be called whenever the state changes.
      /// Defaults to false.
      /// </summary>
      /// <remarks>
      /// The impact on performance of enabling these notifications will depend only entirely on
      /// what the derived class does in its override of OnStateChanged.  That said, enabling this
      /// even with an empty OnStateChanged does have a small but measurable impact.  For example,
      /// one million calls to an empty OnStateChanged takes about 2 seconds on a 3.3GHz CPU.
      /// </remarks>
      public bool EnableStateChangeNotifications { get; set; }

      /// <summary>
      /// Indicates whether <see cref="OnUnhandledEvent"/> should be called when an event is unhandled.
      /// Defaults to false.
      /// </summary>
      public bool EnableUnhandledEventNotifications { get; set; }

      /// <summary>
      /// Indicates whether to enable validation of runtime errors.
      /// </summary>
      public bool EnableValidation { get; set; }

      /// <summary>
      /// Delegate for a method that represents a state in a <see cref="MiniMachine{TEvent}"/>.
      /// </summary>
      /// <param name="e">The event to be processed.</param>
      /// <returns>Null to indicate the event was handled; otherwise, the parent state.</returns>
      public delegate State State(TEvent e);

      enum ExpectedCall
      {
         None = 0,
         TransitionTo,
         DefaultTo
      }

      /// <summary>
      /// Creates instance.
      /// </summary>
      /// <param name="getParentEvent">Event to use to tell a state to return its parent state.</param>
      /// <param name="setDefaultEvent">Event to use to tell a state to set its default substate.</param>
      /// <param name="enterEvent">Event to use to tell a state to perform its entry actions.</param>
      /// <param name="exitEvent">Event to use to tell a state to perform its exit actions.</param>
      /// <param name="maxDepth">Estimated maximum depth of substate nesting.</param>
      protected MiniMachine(TEvent getParentEvent, TEvent setDefaultEvent, TEvent enterEvent, TEvent exitEvent, int maxDepth = 20)
      {
         if (getParentEvent.Equals(setDefaultEvent) ||
             getParentEvent.Equals(enterEvent) ||
             getParentEvent.Equals(exitEvent) ||
             setDefaultEvent.Equals(enterEvent) ||
             setDefaultEvent.Equals(exitEvent) ||
             enterEvent.Equals(exitEvent))
            throw new ArgumentException("Events must all be different but at least two are the same.");

         this.getParentEvent = getParentEvent;
         this.setDefaultEvent = setDefaultEvent;
         this.enterEvent = enterEvent;
         this.exitEvent = exitEvent;

         targetPath = new State[maxDepth];

         expectedCall = ExpectedCall.DefaultTo;
         currentState = Root;
      }

      /// <summary>
      /// Restores the machine to a particular state; intended to be support serialization.
      /// Any state-related data in the derived machine must also be restored.
      /// It is the caller's responsibility to determine how to serialize the current state
      /// (typically based on the name returned by <see cref="GetCurrentState"/>) and how
      /// to map that back onto a state method during deserialization ((typically by using
      /// reflection to find the named method).
      /// No transitions occur and no events are generated as a result of calling this method;
      /// the machine simply assumes it is in the specified state and is immediately ready to 
      /// accept calls to <see cref="Process"/>.
      /// </summary>
      /// <param name="previousState">The state to resume from.</param>
      protected void RestoreToState(State previousState)
      {
         expectedCall = ExpectedCall.None;
         currentState = previousState;
      }

      /// <summary>
      /// Defaults to an immediate substate of the current state.
      /// This must be called by the derived class aftr the machine has been instantiated to set 
      /// the top-level default state.  It must also be called by states which have substates
      /// in response to the <see cref="setDefaultEvent"/> event.
      /// </summary>
      /// <param name="substate">The default substate, which must be an immediate substate of the current state.</param>
      protected void DefaultTo(State substate)
      {
         if (substate == null)
            throw new ArgumentNullException("substate");
         
         if (expectedCall != ExpectedCall.DefaultTo)
            throw new InvalidOperationException("DefaultTo can only be called in response to SetDefault or as the first call after the machine was created.");

         if (EnableValidation)
         {
            var parentState = substate(getParentEvent);
            if (parentState == null)
               throw new InvalidOperationException(string.Format("State '{0}' returned null as its parent.", substate.Method.Name));
            if (parentState != currentState)
               throw new ArgumentOutOfRangeException("substate", string.Format("State '{0}' is not an immediate substate of '{1}'.", parentState.Method.Name, currentState.Method.Name));
         }

         var oldState = currentState;

         // The substate is the new current state, so we enter it.
         currentState = substate;
         substate(enterEvent);

         // Now give substate a chance to do the same for its substate, and so on.
         substate(setDefaultEvent);

         expectedCall = ExpectedCall.None;

         // The very first call to this method is a special case since it's changes the machine's
         // state without going through Process, so we need to notify about that here.
         if (EnableStateChangeNotifications && oldState == Root)
            OnStateChanged(oldState.Method.Name, setDefaultEvent, currentState.Method.Name);
      }

      /// <summary>
      /// Processes the specified event.
      /// </summary>
      /// <param name="e">Event to process.</param>
      protected void Process(TEvent e)
      {
         if (EnableValidation && currentState == Root)
            throw new InvalidOperationException("DefaultTo must be called prior to the first time Process is called.");

         if (EnableValidation && (e.Equals(getParentEvent) || e.Equals(enterEvent) || e.Equals(setDefaultEvent) || e.Equals(exitEvent)))
            throw new ArgumentOutOfRangeException("e", "Built-in events cannot be passed to Process.");

         var oldState = currentState;

         // Send the event to the current state first and if it handles it then
         // great; otherwise, the return will be the parent state, so we give that
         // state a chance to handle it, and so on up the hierarchy.  A state may
         // handle it by performing a transition, in which case currentState gets
         // updated at some point through this loop and by the time we're done
         // with the loop, it will be the correct new value (or unchanged if nobody
         // handled the event).

         expectedCall = ExpectedCall.TransitionTo;
         
         var state = currentState;
         do
         {
            state = state(e);
         } while (state != null && state != Root);

         expectedCall = ExpectedCall.None;

         if (EnableUnhandledEventNotifications && state == Root)
            OnUnhandledEvent(e);

         if (EnableStateChangeNotifications)
            OnStateChanged(oldState.Method.Name, e, currentState.Method.Name);
      }

      /// <summary>
      /// Transitions from the current state to a target state.
      /// </summary>
      /// <param name="target">Target state.</param>
      /// <param name="action">Optional transition action which is executed after 
      /// any exit actions and before any entry actions.</param>
      protected void TransitionTo(State target, Action action = null)
      {
         if (target == null)
            throw new ArgumentNullException("target");

         if (expectedCall != ExpectedCall.TransitionTo)
            throw new InvalidOperationException("TransitionTo can't be called when handling a built-in event.");

         // Do the transition, which entails exiting the source state(s) up to but not 
         // inlucding the Least Common Ancestor, then executing the transition action,
         // and finally entering the target state(s).
         expectedCall = ExpectedCall.None;
         DoTransition(currentState, target, action);

         // Finally, the target must do its default transition if it has any substates.
         // These default transitions "drill down" until it reaches a state that has
         // no substates, which becomes the new "current state".
         expectedCall = ExpectedCall.DefaultTo;
         currentState = target;
         target(setDefaultEvent);
         expectedCall = ExpectedCall.None;
      }

      /// <summary>
      /// Performs the transition from the source state to the target state.
      /// </summary>
      void DoTransition(State source, State target, Action action)
      {
         Transit(source, target, 0, action);
      }

      int Transit(State source, State target, int targetPathIndex, Action action)
      {
         // First we recurse up through the target's ancestors until we reach the root, filling in
         // targetPath so it starts with the target ([0]) and ends with the root ([index]).
         targetPath[targetPathIndex] = target;
         var parent = target(getParentEvent);
         if (parent != null)
         {
            var lca = Transit(source, parent, targetPathIndex + 1, action);

            if (targetPathIndex < lca)
               target(enterEvent);

            return lca;
         }

         if (EnableValidation && target != Root)
            throw new InvalidOperationException(string.Format("State '{0}' returned null as it's parent.", target.Method.Name));

         // We've reached the root so targetPath is complete.
         // Now we need to exit from the source and its parent states until we each
         // the Least Common Ancestor (LCA) between the original source and target.
         // This is an N^2 operation (where N Is the maximum substate depth) but since
         // N is likely to be relatively small (even 8 levels seems pretty damn complex)
         // so N^2 will be reasonable.
         int i;
         do
         {
            // Exit source and then move up to its parent
            source(exitEvent);
            source = source(getParentEvent);

            // Check if source matches any item in the target path (except the target
            // itself which is at [0], so we start at index 1).  If we find a match
            // then that's the Least Common Ancestor.
            i = 1;
            while (i <= targetPathIndex && targetPath[i] != source)
               i++;
         } while (i > targetPathIndex);

         // Perform transition action (if specified)
         if (action != null)
            action();

         return i;
      }

      /// <summary>
      /// The root state.
      /// The top-level states defined by a derived class are direct substates of the root.
      /// To put it another way, the Root is the parent of those top-level states.
      /// </summary>
      // NOTE: Making this static improves performance dramatically!
      protected static State Root(TEvent e)
      {
         return null;
      }

      /// <summary>
      /// Gets the name of the current state.
      /// </summary>
      protected string GetCurrentState()
      {
         return currentState.Method.Name;
      }

      /// <summary>
      /// Gets a string describing the current state.
      /// </summary>
      /// <returns></returns>
      public override string ToString()
      {
         return string.Format("CurrentState={0}", GetCurrentState());
      }

      /// <summary>
      /// When overridden in a derived class, does something when the state has changed.
      /// This is intended to implement features such as logging or audting.
      /// This implementation does nothing.
      /// </summary>
      /// <param name="oldState">The old state.</param>
      /// <param name="e">The event that caused the state change.</param>
      /// <param name="newState">The new state.</param>
      protected virtual void OnStateChanged(string oldState, TEvent e, string newState)
      {
      }

      /// <summary>
      /// When overridden in a derived class, does something when an event was not handled.
      /// This implementation ignores the unhandled event.  It is up to the derived class
      /// to determine whether this represents a normal or abnormal situation.
      /// </summary>
      /// <param name="e">The unhandled event.</param>
      protected virtual void OnUnhandledEvent(TEvent e)
      {
      }
   }
}
